前篇認識到 PyTorch 語法簡潔好上手、且實作上基於 dynamic computation graph 讓架構能在訓練時改變。接下來兩篇會以概略簡介和實作範例帶大家入門 PyTorch。
安裝 PyTorch 有很多種方式,因為會使用 Anaconda 幫我們管理環境,所以會透過他安裝。
請按照 Python 3 的版本安裝!
第一步請先到這裡選擇自己的作業系統,按照步驟安裝 Anaconda。
接著打開 terminal 跑下列指令:
# 建立一個名為 pytorch-tutorial 且使用 Python 3.8 的新環境
#(或其他 Python 3 版本)
$ conda create -n pytorch-tutorial python=3.8
# 檢查環境已被創立
$ conda env list # 會看到現在在 base,且另有剛剛建立的 pytorch-tutorial
# 進入剛剛創建的環境
$ conda activate pytorch-tutorial
# 透過 conda 安裝 PyTorch
# 請至 PyTorch 官網:https://pytorch.org/ 選好自己的環境選項,
# 複製他提供的 command 上來跑
$ conda install pytorch torchvision -c pytorch
來確認一下 Pytorch 有沒有安裝成功:
# 可以透過 Python interpreter 試著 import torch 來確認 PyTorch 安裝成功
$ python
Python 3.8.5 (default, Sep 4 2020, 02:22:02)
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>>
沒有出現 error 且出現下個 prompt 即安裝成功!Ctrl-D
可以離開 interpreter。
—— 這是我在 MacOS 上的設定。
不知道 CUDA 是什麼的,他是讓 model 可以在 GPU 上訓練的接口。對新手來說還不需要,可以就選 None。
這個 cheatsheet 列了一些比較基本常用的 conda command,可以參考。
剛剛看到的 torch
就是 PyTorch 的 package,接下來就要來看看他底下有哪些 subpackage 用來完成一個 network 的建立和訓練。
大致上從 creation 到 training 的實作流程如下:
Train 完做 evaluation 則大致上是:
通常會把 training 和 testing 分開來寫比較好各自跑。兩邊 data loading 有重複可以寫在一個 file 重複利用。
下一篇會實際走過手寫辨識實作來熟悉流程。不過在這之前,先來簡單瞭解一下 PyTorch 提供的一些 package 和 API,方便之後講解。都只是簡單舉例了解概念,實際操作需要大量搜尋 documentation 理解用法。
torch.tensor
是 PyTorch 最核心的 data type。Scalar 是 0 維、vector 是 1 維、matrix 是 2 維、tensor 則可以是任意維度,而在 neural network 裡 data 基本上都需要轉化為 tensor 處理。
他的很多 API 都跟 Numpy 類似。
>>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float)
>>> x
tensor([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
>>> y = torch.eye(3)
>>> y
tensor([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])
>>> z = torch.zeros(3, 3)
>>> z
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
>>> x + y
tensor([[ 2., 2., 3.],
[ 4., 6., 6.],
[ 7., 8., 10.]])
>>> x.pow(2)
tensor([[ 1., 4., 9.],
[16., 25., 36.],
[49., 64., 81.]])
>>> x.matmul(x) # or, x @ x
tensor([[ 30., 36., 42.],
[ 66., 81., 96.],
[102., 126., 150.]])
>>> x.sum()
tensor(45.)
>>> x.sum(dim=0)
tensor([12., 15., 18.])
>>> x.sum(dim=1)
tensor([ 6., 15., 24.])
>>> x.shape
torch.Size([3, 3])
>>> x.unsqueeze(0)
tensor([[[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]]])
>>> x.unsqueeze(0).shape
torch.Size([1, 3, 3])
>>> a = torch.rand(2, 3)
>>> a
tensor([[0.4079, 0.4236, 0.2162],
[0.8427, 0.1488, 0.8783]])
>>> a.transpose(0, 1) # 交換 dimension 0, 1
tensor([[0.4079, 0.8427],
[0.4236, 0.1488],
[0.2162, 0.8783]])
>>> a.permute(1, 0) # 讓 dimension 依照指定排列
tensor([[0.4079, 0.8427],
[0.4236, 0.1488],
[0.2162, 0.8783]])
>>> b = torch.rand(2, 3, 2)
>>> b.size()
torch.Size([2, 3, 2])
>>> b.view(3, 2, 2).size() # 轉成 3 x 2 x 2
torch.Size([3, 2, 2])
>>> b.view(3, -1).size() # -1 代表根據其他 dimension 決定這個 dimension 剩下的 size
torch.Size([3, 4])
之前當助教的時候,學生特別容易搞混
view
、transpose
、permute
的使用,以為隨便用一個來讓 data 變成適當形狀運算即可,而忽略了數學上的含義。這還滿危險的,因為形狀對了程式就能跑下去,但最後發現訓練不好時,也很難找到哪裡出錯。基本上
transpose
和permute
功能類似,都是數學上對 matrix 做 transpose 交換 dimension 的意義。但view
沒有這層含義,比較類似單純把同一份 data 揉成不同形狀。例如[[1, 2, 3], [4, 5, 6]]
揉成[[1, 2], [3, 4], [5, 6]]
會用view
,但transpose
兩個 dimension 會變成[[1, 4], [2, 5], [3, 6]]
,本質上已經不同。可以參考 [1] 更了解他們的差異,以免誤用。
有了 tensor,把他們串聯成 network 就成了 computation graph。他們之間的關係會被保存,如此才能在 backward propagation 時知道如何取 gradient。
舉例來說,我們建一個小小 computation graph,為兩個 node 相加:
>>> a = torch.tensor([1., 2.], requires_grad=True)
>>> b = torch.tensor([3., 4.], requires_grad=True)
>>> c = a + b
requires_grad = True
才能計算 gradient。
我們取 loss 並呼叫 loss.backward()
,即可計算 computation graph 裡 parameters 的 gradient:
>>> loss = c.sum()
>>> loss.backward()
>>> a.grad
tensor([1., 1.])
>>> b.grad
tensor([1., 1.])
怎麼知道如何計算 a
和 b
的 gradient 的呢?因為 c
有記錄他與 a
和 b
的關係是 sum。來看一下他的 gradient function:
>>> c.grad_fn
<AddBackward0 object at 0x7f970004f460>
很方便吧。這也是 chain rule 的力量喔。
收納在 torch.utils.data
底下的實用工具,提供一些方便處理 data preprocessing 的功能。
主要集大成的 dataloader,定義 dataset 來源、batch size、要不要 shuffle 等等。
使用方式很簡單,定義完後在 training 或 testing 時用 for loop 簡單 iterate 過去:
loader = DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
batch_sampler=None, num_workers=0, collate_fn=None,
pin_memory=False, drop_last=False, timeout=0,
worker_init_fn=None)
for batch in loader:
# train or test with batch
分為 map-style 和 iterable-style dataset。會需要把 data 從電腦 load 進來,preprocess 之後打包好。
__getitem__()
和 __len__()
dataset[idx]
的方式取用__iter__()
iter(dataset)
的方式取用__iter__()
裡舉例來說,可以這樣使用:
class MyIterableDataset(torch.utils.data.IterableDataset):
def __init__(self, start, end):
super(MyIterableDataset).__init__()
self.n = None
def __iter__(self):
dataset = read_data_from_file()
self.n = len(dataset)
return iter(dataset)
決定 data loading 的順序。適用於 map-style dataset。
__iter__()
和 __len__()
收納於 torch.nn.functional
、torch
、torch.nn
,有各式常用 activation function、loss function、或常用 function 等等。
如果你點進去看的話,會發現有很多重複的 function,例如 sigmoid 有 torch.sigmoid
、torch.nn.functional.sigmoid
、torch.nn.Sigmoid
,非常受歡迎。他們之間有什麼差別嗎?
以 sigmoid 來說,基本上除了前兩個是 Python function 而後面的是 Module class 所以使用上有些不同外,底下會呼叫同樣的 function,所以效果是一樣的。那什麼時候該用哪邊提供的 function 比較好呢?
有三種情況會比較推薦用 torch.nn
:
torch.nn
底下提供的 Module,因為他是一個 class 裡面會存 variables。例如 Conv2d
、Linear
等等。Dropout
會在 training 時 enable,testing 時 disable,就適合用 torch.nn
底下提供的 Module,因為內部會幫你檢查是在做 training 或 testing。torch.nn.Sequential
串聯在一起,建立簡單一進一出的 network。因為一定要用 Module class,像 sigmoid 這種 function 也必須用 torch.nn.Sigmoid
定義才能加進去。如果沒有 state、training testing 行為一致、不用 Sequential,那麼可以使用 torch
或 torch.nn.functional
底下的 function。不過如果一時判斷不清或懶得判斷,就都用 torch.nn
吧。
至於 torch
和 torch.nn.functional
之間的差別,比較在於歷史發展下來的結果,基本上沒有差別。如果 torch
底下有的話就用 torch
的,因為很多在 torch.nn.functional
都被 deprecate 了。
可以參考 [2][3] 提供的解釋。
使用方法大概為:
>>> a = torch.rand(2)
>>> a
tensor([0.0530, 0.7934])
>>> torch.sigmoid(a)
tensor([0.5133, 0.6886])
那我們就來舉例一些 function。
收錄在 torch.optim
底下眾多的 optimizer。前面介紹 optimizer 時的數學都含在裡面了,只需要挑一個效果好的。
Optimizer 可以這樣定義:
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
而訓練時會用 optimizer.zero_grad()
把前面的 gradient 歸零(否則會累積前一輪的),等 loss 算完後 call loss.backward()
按照 computation graph 往前計算每個 parameter 的 gradient,最後用 optimizer.step()
按照各自的 optimization 方法進行 update:
for input, target in dataset:
optimizer.zero_grad()
output = model(input)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
最後介紹主要建立 network 的骨幹,torch.nn.Module
。
要建立自己的 network 很簡單的三步驟:繼承 Module class、overwrite __init__()
來定義 network 架構、 和 overwrite forward()
來定義 feed forward 時接收 input 後怎麼產生 output:
class Model(nn.Module): # 繼承 Module
def __init__(self):
super(Model, self).__init__()
# 定義整個 network 的架構,這邊有兩層 fully-connected layer
self.fc1 = nn.Linear(20, 30) # 20 x 30
self.fc2 = nn.Linear(30, 5) # 30 x 5
def forward(self, x):
# 接收 input x 之後,經過兩層 fully-connected layer
# 和 activation function 取得 output
x = F.relu(self.fc1(x))
return F.relu(self.fc2(x))
Fully-connected layer 就是在兩層 node 之間,每個 node 與 node 之間都有連結。
如此一來在 training 時把 data 傳進 model 就可以取得 output:
x = torch.rand(20)
m = Model()
y = m(x)
而 network 中的很多 layer 也都是 Module,因為他們也都是有輸入輸出和 parameters 的 network,例如上面看到的 Linear layer。此外還有很多不同架構的 layer,用來應付不同需求,例如 Convolutional layer 適合在 image 提取特徵、RNN layer 適合提取時間序列的特徵。之後會再詳細介紹!
很多都有些複雜,一開始不太知道怎麼傳參數的話很正常,使用的時候就多多參考 documentation 或網路範例。
下面的問題在用程式查看之前,鼓勵大家努力查 doc 想想看。
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
。那麼 x.sum(dim=0)
結果為何?x.transpose(0, 1)
結果為何?y
形狀為何?